﻿package com.dcliment.video
{
	
	import com.moody.utils.TimeConverter;
	
	import flash.display.Sprite;
	import flash.events.*;
	import flash.media.SoundTransform;
	import flash.media.Video;
	import flash.net.NetConnection;
	import flash.net.NetStream;
	import flash.utils.Timer;
	
	/**
	 * @author dcliment
	 *
	 * version 2.2;
	 * Added ability to seek at the point the video allows it.
	 * NOTE :: Changed get metaData() to getMetaData() cause it was conflicting with a Sprite Fucntion (only in air 3.7)
	 */	
	public class VideoPlayer extends Sprite 
	{
		
		public static const STREAM_READY			:String = "STREAM_READY"; // Means NetConnection was a success
		public static const STREAM_LOADED			:String = "STREAM_LOADED"; // BUFFER FULL
		public static const STREAM_SEEKED			:String = "STREAM_SEEKED"; // video Seeked
		public static const STREAM_PLAYING			:String = "STREAM_PLAYING";
		public static const STREAM_STOPPED			:String = "STREAM_STOPPED";
		public static const STREAM_BUFFERING		:String = "STREAM_BUFFERING";
		public static const STREAM_BUFFERING_STOP	:String = "STREAM_BUFFERING_STOP";
		public static const STREAM_PLAYHEAD_UPDATE	:String = "STREAM_PLAYHEAD_UPDATE";
		public static const META_DATA_RECEIVED		:String = "META_DATA_RECEIVED";
		public static const CUE_POINT_FIRED			:String = "CUE_POINT_FIRED";
		
		private var mCommand:String;
		private var mURL:String;
		private var mDurationCode:String;
		
		private var mIsReady:Boolean = false;
		private var mMute:Boolean = false;
		private var mAutoplay:Boolean;
		private var mIsPlaying:Boolean;
		private var mInitializing:Boolean = true;
		private var mCompleted:Boolean = false;
		
        private var mConnection:NetConnection;
        private var mStream:NetStream;
		private var mVolTransform:SoundTransform = new SoundTransform();
		private var mPlayheadTimer:Timer;
		private var mVideo:Video;
		private var mMetaData:Object;
		
		private var videoOrgWidth:Number;
		private var videoOrgHeight:Number;
		private var mWidth:Number;
		private var mHeight:Number;
		private var mLastSetVolume:Number = .5;
		private var mDefaultVolume:Number = .5;
		private var mDuration:Number;
		private var mPlayheadTime:Number;
		private var mSeek:Number;
		// CuePoints
		private var _cuePoints:Array = new Array();
		private var _activeCuePointTime:Number;
		private var _activeCuePointName:String;		
		
		/**
		 * Created an empty constructor that allows you to have a videoPlayer object instantiated and event listeners added to it even without a video.
		 * 
		 * ex:
		 * var _videoPlayer:VideoPlayer = new VideoPlayer();
		 * 	   _videoPlayer.createPlayer("url", 800, 600, true);
		 * 	   _videoPlayer.init();
		 * 	   addChild(_videoPlayer);
		 */		
		
		public function VideoPlayer():void 
		{
			super();
		}
		
		/**
		 * 
		 * @param videoURL
		 * @param w :: Optional if not set will take the width of the video's src
		 * @param h :: Optional if not set will take the width of the video's src
		 * @param autoplay
		 * @param seek :: if you need to seek to a sepcific point in the video if; ( i believe autoPlay has to be = true for this to work);
		 * @param command
		 * 
		 */		
		//-------------------------------------------------------------------------------------------
		public function createPlayer(videoURL:String, w:Number = NaN, h:Number = NaN, autoplay:Boolean=false, seek:Number = NaN, command:String=null):void 
		{
			mURL = videoURL;
			mWidth = w;
			mHeight = h;
			mAutoplay = autoplay;
			mSeek = seek;
			mCommand = command;
		}
		
		// -------------------------------------------------------------------------------------------
		public function init():void 
		{
			
			mCommand = mCommand == "" ? null : mCommand
			
			mPlayheadTimer = new Timer(100);
			addEventListener(Event.REMOVED_FROM_STAGE, unloadHandler, false, 0, true);
			
			var client:Object = {};
				client.onBWDone = onBWDone;
			
			mConnection = new NetConnection();
			mConnection.addEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler, false, 0, true);
			mConnection.addEventListener(NetStatusEvent.NET_STATUS, netConnectionStatusHandler, false, 0, true);
			mConnection.client = client;
			mConnection.connect(mCommand);
		}
		
		// -------------------------------------------------------------------------------------------
		
        private function createVideo():void 
		{	
			
			var client:Object = {};
				client.onMetaData = nsMetaDataHandler;
				client.onCuePoint = nsCuePointHandler;
			
			// Stream
			mStream = new NetStream(mConnection);
			mStream.bufferTime = 5;
			//mStream.inBufferSeek = true;
			mStream.client = client;
			// Events
            mStream.addEventListener(NetStatusEvent.NET_STATUS, netStreamStatusHandler, false, 0, true);
            mStream.addEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler, false, 0, true);
			
			// Create video
            mVideo = new Video();
			mVideo.smoothing = true;
			// width and height are added when metaData is received
			
			addChild(mVideo);
			
			setVolume(mDefaultVolume);
			
			mVideo.attachNetStream(mStream);
			
			// AUTOPLAY
			if(mAutoplay) 
				load(mURL);
			
			if(!isNaN(mSeek))
			{
				this.addEventListener(VideoPlayer.META_DATA_RECEIVED, onStreamReadySeek, false, 0, true);
			}
			
        }
		private function onStreamReadySeek(e:Event):void
		{
			this.removeEventListener(VideoPlayer.META_DATA_RECEIVED, onStreamReadySeek);
			
			this.seek(mSeek);
		}
		
		// -------------------------------------------------------------------------------------------		
		public function play():void 
		{
			mStream.play(mURL);
		}
		
		public function restart():void 
		{
			mStream.play(mURL);
		}
		
		public function load(url:String):void 
		{		
			dispose();
			mStream.play(url);
			
		}	
		
		public function togglePlay():void 
		{
			if(mIsPlaying)
				pause();
			else
				resume();
		}
		
		public function resume():void 
		{
			mStream.resume();
			mPlayheadTimer.start();
			mIsPlaying = true;
		}
		
		public function pause():void 
		{
			mStream.pause();
			mIsPlaying = false;
		}
		
		public function seek(offset:Number):void 
		{
			//trace("Seeked to + " + offset);
			//mStream.pause();
			mStream.seek(offset);
		}
		
		public function reset(url:String=null):void 
		{
			if(url) 
			{
				mURL = url;
			}
			if(mIsReady) {
				pause();
				seek(0);
				resetActiveCuePoint();
			}
		}
		
		public function dispose():void 
		{
			if(!mStream) // Fail safe if Player hadn't been created yet
				return;
			
			mStream.dispose();
			mMetaData = null;
			
			resetActiveCuePoint();
			
			
		}
		
		public function close():void 
		{
			if(!mStream) // Fail safe if Player hadn't been created yet
				return;
			
			mStream.close();
			mConnection.close();
			mIsReady = false;
			mPlayheadTimer.stop();
			mPlayheadTimer.removeEventListener(TimerEvent.TIMER, playheadUpdateHandler);
			removeEventListener(Event.REMOVED_FROM_STAGE, unloadHandler);
			mStream.removeEventListener(NetStatusEvent.NET_STATUS, netStreamStatusHandler);
            mStream.removeEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler);
			mPlayheadTimer.removeEventListener(TimerEvent.TIMER, playheadUpdateHandler);
			mConnection.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
			mConnection.removeEventListener(NetStatusEvent.NET_STATUS, netConnectionStatusHandler);
			
			mURL = null;
			
			resetActiveCuePoint();
			
			if(this.hasEventListener(VideoPlayer.STREAM_PLAYHEAD_UPDATE))
				this.removeEventListener(VideoPlayer.STREAM_PLAYHEAD_UPDATE, checkForCuePoints);
		}
		
		// Status Handlers ---------------------------------------------------------------------------
		
		private function netConnectionStatusHandler(event:NetStatusEvent):void 
		{
			//trace("** Connection Status: " + event.info.code);
			switch (event.info.code) {
                case "NetConnection.Connect.Success":
					
					createVideo();
					//trace("VideopPlayer.netConnectionStatusHandler() ::NetConnection Success");
					dispatchEvent(new Event(STREAM_READY));
					mIsReady = true;
                    break;
            }			
        }
		
		private function netStreamStatusHandler(event:NetStatusEvent):void 
		{
			//trace("** Stream Status: " + event.info.code);
			switch (event.info.code) {
				/*case "NetStream.Play.COMPLETE":
					trace("VID COMPLETE!!!!");
					mPlayheadTimer.stop();
					dispatchEvent(new Event(Event.COMPLETE));
					mIsPlaying = false;
					break;*/
				case "NetStream.Play.Reset":
					mPlayheadTimer.start();
					dispatchEvent(new Event(STREAM_BUFFERING));
					mIsPlaying = true;
					mCompleted = false;
					break;
				case "NetStream.Play.Start":
					//trace("START");
					mPlayheadTimer.start();
					mIsPlaying = true;
					mCompleted = false;
					dispatchEvent(new Event(STREAM_PLAYING));	
                    break;					
				case "NetStream.Pause.Notify":
					mPlayheadTimer.stop();
					dispatchEvent(new Event(STREAM_STOPPED));					
                    break;
				case "NetStream.Unpause.Notify":
					mPlayheadTimer.stop();
					dispatchEvent(new Event(STREAM_PLAYING));					
					break;
				case "NetStream.Play.Stop":
					mPlayheadTimer.stop();
					mCompleted = true;
					mIsPlaying = false;
					dispatchEvent(new Event(Event.COMPLETE));
                    break;
				case "NetStream.Seek.Notify":
					dispatchEvent(new Event(STREAM_SEEKED));
					if (!mIsPlaying) { pause(); }
                    break;					
				case "NetStream.Buffer.Empty":
					//if(mInit){
						//dispatchEvent(new Event(Event.COMPLETE));
						//enableProgress();
						//mInit = false;
					//}
                    break;
				case "NetStream.Buffer.Full":
					if (mInitializing) { 
						dispatchEvent(new Event(STREAM_LOADED));	
						mInitializing = false; 
					}
                    break;
            }
        }
		
		public function toggleMute():void 
		{
			mMute = !mMute;
			mVolTransform.volume = mMute ? 0 : mLastSetVolume;
			mStream.soundTransform = mVolTransform;
		}
		
		private function mute():void 
		{
			mMute = true;
			mVolTransform.volume = 0;
			mStream.soundTransform = mVolTransform;
			mLastSetVolume = mDefaultVolume;
		}
		
		public function setVolume(value:Number):void 
		{
			if(value < 0)
				value = 0;
			else if(value > 1)
				value = 1;
				
			if(value == 0)
				mute();
			else{
				mMute = false;
				mVolTransform.volume = value;
				mStream.soundTransform = mVolTransform;
				mLastSetVolume = value;
			}
		}
		
		// -------------------------------------------------------------------------------------------
		
		private function nsMetaDataHandler(data:Object):void 
		{
			
			mMetaData = data;
			
			mDuration = data.duration;
			mDurationCode = TimeConverter.convert(mDuration);
			mPlayheadTimer.addEventListener(TimerEvent.TIMER, playheadUpdateHandler, false, 0, true);
			
			videoOrgWidth = data.width;
			videoOrgHeight = data.height;
			
			mVideo.width = (mWidth) ? mWidth : videoOrgWidth;
			mVideo.height = (mHeight) ? mHeight : videoOrgHeight;
			
			/* Testing
			for(var key:String in data)
				trace(key, data[key]);
			*/
			
			dispatchEvent(new Event(META_DATA_RECEIVED));
		}
		/**
		 * This is a cuePoint Handler for embedded cuePoints in the netStream (very few videos will be delivered this way);
		 * 
		 * @param item 
		 */		
		private function nsCuePointHandler(item:Object):void 
		{
			//trace("cuePoint");
			//trace(item.name + "\t" + item.time);
			
			this._activeCuePointName = item.name;
			this._activeCuePointTime = item.time;
			
			dispatchEvent(new Event(VideoPlayer.CUE_POINT_FIRED));
			
		}
		
		private function playheadUpdateHandler(e:Event):void 
		{
			/*if(mIsPlaying && Math.ceil(mDuration) == Math.ceil(mStream.time)){
				mIsPlaying = false;
				dispatchEvent(new Event(Event.COMPLETE));
			}*/
			dispatchEvent(new Event(STREAM_PLAYHEAD_UPDATE));
				
			if (!mInitializing) {
				if (mPlayheadTime == mStream.time && mIsPlaying) 
				{
					dispatchEvent(new Event(STREAM_BUFFERING));
				} 
				else {
					dispatchEvent(new Event(STREAM_BUFFERING_STOP));
				}
			}
		}
		
		
		
		// -------------------------------------------------------------------------------------------
		
        private function securityErrorHandler(event:SecurityErrorEvent):void 
		{
			trace("VideoPlayer.securityErrorHandler() :: " + event.text + " : " + event.errorID);
		}		        
        
		private function asyncErrorHandler(event:AsyncErrorEvent):void 
		{
			trace("asyncErrorHandler: " + event.text + " : " + event.error);
		}		
		
		private function onBWDone(... args):void 
		{
		
		}
		
		// -------------------------------------------------------------------------------------------		
		
		private function unloadHandler(e:Event):void 
		{
			
			mStream.close();
			mConnection.close();
			
			removeEventListener(Event.REMOVED_FROM_STAGE, unloadHandler);
			mStream.removeEventListener(NetStatusEvent.NET_STATUS, netStreamStatusHandler);
            mStream.removeEventListener(AsyncErrorEvent.ASYNC_ERROR, asyncErrorHandler);
			mPlayheadTimer.removeEventListener(TimerEvent.TIMER, playheadUpdateHandler);
			mConnection.removeEventListener(SecurityErrorEvent.SECURITY_ERROR, securityErrorHandler);
			mConnection.removeEventListener(NetStatusEvent.NET_STATUS, netConnectionStatusHandler);
			
			if(this.hasEventListener(VideoPlayer.STREAM_PLAYHEAD_UPDATE))
				this.removeEventListener(VideoPlayer.STREAM_PLAYHEAD_UPDATE, checkForCuePoints);
			
		}
		
		///////////////////////////////
		// CUEPOINTS
		/////////////////////////////
		public function addCuePoint($time:Number, $name:String = ""):void
		{
			_cuePoints.push([$time, $name]);
			
			// this tells the videoPlayer that cuePoints have been added so to listen for them
			if(!this.hasEventListener(VideoPlayer.STREAM_PLAYHEAD_UPDATE))
				this.addEventListener(VideoPlayer.STREAM_PLAYHEAD_UPDATE, checkForCuePoints, false, 0, true);
		}
		private function checkForCuePoints(e:Event):void
		{
			//trace("playHeadTime = " + this.playheadTime);
			//trace("playHead :: FLOORED :: = " + Math.floor(this.playheadTime));
			
			for each(var cuePoint:Array in _cuePoints)
			{
				
				if(cuePoint[0] == Math.floor(this.playheadTime) && cuePoint[0] != this._activeCuePointTime)
				{
					trace("VideoPlayer.checkForCuePoints() FIRED :: time = " + cuePoint[0] + " :: Name = " + cuePoint[1]);
					
					this._activeCuePointTime = cuePoint[0];
					this._activeCuePointName = cuePoint[1];
					
					dispatchEvent(new Event(VideoPlayer.CUE_POINT_FIRED));
					
					break;
				}
			}
		}
		/**
		 * Has to be set as an array of arrays 
		 * ex: cuepoint[[time, name], [time, name]];
		 * 
		 * @param value
		 * 
		 */		
		public function set cuePoints(value:Array):void
		{
			_cuePoints = value;
			
			// this tells the videoPlayer that cuePoints have been added so to listen for them
			this.addEventListener(VideoPlayer.STREAM_PLAYHEAD_UPDATE, checkForCuePoints, false, 0, true);
		}
		
		public function get activeCuePointTime():Number
		{
			return _activeCuePointTime;
		}
		
		public function get activeCuePointName():String
		{
			return _activeCuePointName;
		}
		
		public function resetActiveCuePoint():void
		{
			_activeCuePointTime = NaN;
			_activeCuePointName = null;
		}
		////////////////////////////////
		// ACCESSORS
		///////////////////////////////
		public function get isReady():Boolean 
		{
			return mIsReady;
		}
		public function get progress():Number 
		{
			return mStream.time/mDuration;
		}
		public function get timecode():String 
		{ 
			return TimeConverter.convert(mStream.time) + " / " + mDurationCode; 
		}
		
		public function get currentTimeCode():String 
		{
			//trace(mStream.time);
			//trace(TimeConverter.convert(mStream.time));
			
			return TimeConverter.convert(mStream.time);
		}
		
		public function get isPlaying():Boolean 
		{ 
			return mIsPlaying; 
		}
		
		public function get playheadTime():Number 
		{ 
			return mStream.time; 
		}		
		
		public function get totalTime():Number 
		{ 
			return mDuration; 
		}
		
		public function get totalTimeCode():String 
		{
			return mDurationCode;
		}
		
		public function get bufferLength():Number
		{ 
			return mStream.bufferLength;
		}
		
		public function get bufferTime():Number 
		{ 
			return mStream.bufferTime;
		}
		
		public override function set width(value:Number):void 
		{
			mVideo.width = value;
		}
		public override function get width():Number
		{
			return mVideo.width;
		}
		
		public override function set height(value:Number):void 
		{
			mVideo.height = value;
		}
		public override function get height():Number
		{
			return mVideo.height;
		}
		public function get url():String
		{
			return mURL;
		}
		/**
		 * used to be get metaData() but that is a super fucntion of sprite i guess so changed to getMetaData()
		 * 
		 * @return  
		 */		
		public function getMetaData():Object
		{
			return mMetaData;
		}
		
		/**
		 * If video completely played through this will be true
		 * @return  
		 */		
		public function get completed():Boolean
		{
			return mCompleted;
		}
		
		// -------------------------------------------------------------------------------------------
	}
}